Skip to main content

React의 불변성

React에서 불변성을 지켜야 하는 이유

React를 쓰다 보면 한 번쯤은 이런 말을 들어보셨을겁니다.

"React에서는 상태를 직접 변경하지 말고, 불변성을 지켜야 합니다."

처음엔 솔직히 잘 와닿지 않았습니다. 값만 바뀌면 되는 거 아닌가 싶고, 굳이 매번 복사해서 새 객체를 만드는 게 귀찮게 느껴지기도 합니다.

하지만 이 규칙은 단순한 컨벤션이 아니라 React의 렌더링 방식과 성능 전략의 핵심 전제입니다.

이 글에서는 React가 왜 불변성을 전제로 설계되었는지, 그리고 불변성을 지키지 않으면 어떤 문제가 생기는지를 정리해보려고 합니다.

불변성(Immutability)이란 기존 값을 직접 수정하지 않고, 새로운 값을 만들어 교체하는 방식을 말합니다.

state.count = state.count + 1; // ❌
setState({ ...state, count: state.count + 1 }); // ✅

React는 상태 변경을 어떻게 감지할까?

React는 상태 변경을 판단할 때 깊은 비교를 하지 않습니다. 이전 상태와 다음 상태의 참조만 비교합니다.

prevState === nextState;

이 방식은 불변성을 전제로 할 때만 안전합니다.

불변성을 지키지 않으면 생기는 문제

1. 리렌더링이 발생하지 않는다

객체를 직접 수정하면 참조가 유지되어 React는 변경을 감지하지 못합니다.

2. memo 최적화가 깨진다

React.memo, useMemo, useCallback 모두 참조 비교 기반입니다.

3. 상태 추적이 어려워진다

이전 상태를 신뢰할 수 없게 되면서 디버깅 난이도가 올라갑니다.

왜 React는 참조 비교를 선택했을까?

왜 React는 값이 같은지를 깊게 비교하지 않고, 참조만 비교할까?

객체 내부 값까지 전부 비교해주면 더 정확하지 않을까 싶지만 React는 의도적으로 참조 비교(reference equality) 를 선택했습니다.

이 선택은 단순한 구현상의 편의가 아니라 Reconciliation과 렌더링 성능을 고려한 구조적인 결정입니다.


React는 상태나 props 변경 여부를 판단할 때 기본적으로 Object.is를 사용한다.

Object.is(prevState, nextState);

Object.is===와 거의 비슷하지만 몇 가지 차이가 있다.

Object.is(NaN, NaN); // true
NaN === NaN; // false

Object.is(+0, -0); // false
+0 === -0; // true

React는 값의 의미를 최대한 보존하면서도 빠르게 비교할 수 있는 방법이 필요했고, 그 기준으로 Object.is가 선택됐다.

중요한건 React는 객체 내부를 비교하지 않는다.


깊은 비교를 하지 않는 이유

1. 비용이 너무 크다

객체나 배열을 깊게 비교하려면:

  • 모든 키 순회
  • 중첩 구조 재귀 탐색
  • 순환 참조 처리

이 과정은 렌더링마다 수행되기엔 비용이 너무 크다.

UI는 생각보다 자주 업데이트된다. 이때마다 deep compare를 한다면 성능은 급격히 나빠질 수밖에 없다.


2. 어디까지 비교해야 할지 애매하다

const state = {
user: {
profile: {
name: "Jin",
age: 27,
},
},
};
  • name만 바뀌면?
  • age만 바뀌면?
  • 내부 객체 참조는 그대로면?

어디까지를 "변경"으로 볼지 기준을 정하는 순간 React는 너무 많은 정책 결정을 떠안게 된다.

그래서 React는 "변경되었으면, 새로운 참조를 만들어라."가 된것이다.


3. 깊은 비교를 했다면?

겉보기엔 편해 보이지만, 실제로는 문제가 많다.

  • 렌더링 비용 예측 불가
  • 대규모 트리에서 성능 급락
  • 최적화 포인트가 불명확

React는 "마법처럼 알아서 해주는 라이브러리"가 아니라 "예측 가능한 규칙을 가진 UI 엔진"을 목표로 한다.

참조 비교를 하지 않았다면 못했을 것이다.


불변성과 참조 비교의 관계

React의 핵심 가정은

  • 상태는 불변성을 지켜 업데이트된다
  • 변경이 발생하면 참조가 바뀐다
setState((prev) => ({
...prev,
count: prev.count + 1,
}));

이 패턴이 보장되면 React는 참조 비교만으로도 충분히 변경 여부를 판단할 수 있다.

  • 비교 비용 O(1)
  • 구현 단순
  • 예측 가능한 동작

불변성 덕분에 React는 O(1) 비교로 빠르게 변경 여부를 판단할 수 있습니다.

이 참조 비교 전략은 Virtual DOM을 비교하는 Reconciliation 단계에서도 그대로 쓰인다.


Reconciliation에서 이 선택이 어떻게 쓰일까?

Reconciliation은 이전 Virtual DOM과 새로운 Virtual DOM을 비교하는 과정이다.

이때 React는 다음과 같은 전략을 사용한다.

  1. 타입이 다르면 → 트리 전체 교체
  2. key가 다르면 → 다른 요소로 판단
  3. props 참조가 같으면 → 하위 비교 스킵 가능

특히 React.memo는 이 전략을 그대로 활용한다.

const Component = React.memo(function Component(props) {
return <div />;
});
  • 이전 props와 다음 props를 Object.is로 비교
  • 같으면 렌더링 생략

즉, 참조 비교는 Reconciliation 성능 최적화의 핵심 도구다.


자주 쓰는 불변성 패턴

setState((prev) => ({ ...prev, value: newValue }));
setList((prev) => prev.filter((item) => item.id !== id));

정리

React가 참조 비교를 선택한 이유는 명확하다.

  • 빠르고(O(1))
  • 단순하며
  • 예측 가능하기 때문이다

이를 가능하게 만드는 전제가 바로 불변성이고, 이 구조 위에서 Reconciliation과 각종 최적화가 돌아간다.

React의 성능은 비교를 똑똑하게 해서 얻은 게 아니라 비교를 단순하게 만들어서 얻은 것이다.